import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.StringReader; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketAddress; import java.security.KeyPair; import java.security.Security; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.crypto.Cipher; import java.security.MessageDigest; import java.math.BigInteger; import org.apache.commons.codec.binary.Base64; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.openssl.PEMReader; /** * An primitive RTSP responder for replying iTunes * @author bencall * */ public class RTSPResponder extends Thread { private Socket socket; // Connected socket private int[] fmtp; private byte[] aesiv, aeskey; // ANNOUNCE request infos private AudioServer serv; // Audio listener byte[] hwAddr; private BufferedReader in; private String password; private RTSPResponse response; // Pre-define patterns private static final Pattern authPattern = Pattern.compile("Digest username=\"(.*)\", realm=\"(.*)\", nonce=\"(.*)\", uri=\"(.*)\", response=\"(.*)\""); private static final Pattern completedPacket = Pattern.compile("(.*)\r\n\r\n"); private static final String key = "-----BEGIN RSA PRIVATE KEY-----\n" +"MIIEpQIBAAKCAQEA59dE8qLieItsH1WgjrcFRKj6eUWqi+bGLOX1HL3U3GhC/j0Qg90u3sG/1CUt\n" +"wC5vOYvfDmFI6oSFXi5ELabWJmT2dKHzBJKa3k9ok+8t9ucRqMd6DZHJ2YCCLlDRKSKv6kDqnw4U\n" +"wPdpOMXziC/AMj3Z/lUVX1G7WSHCAWKf1zNS1eLvqr+boEjXuBOitnZ/bDzPHrTOZz0Dew0uowxf\n" +"/+sG+NCK3eQJVxqcaJ/vEHKIVd2M+5qL71yJQ+87X6oV3eaYvt3zWZYD6z5vYTcrtij2VZ9Zmni/\n" +"UAaHqn9JdsBWLUEpVviYnhimNVvYFZeCXg/IdTQ+x4IRdiXNv5hEewIDAQABAoIBAQDl8Axy9XfW\n" +"BLmkzkEiqoSwF0PsmVrPzH9KsnwLGH+QZlvjWd8SWYGN7u1507HvhF5N3drJoVU3O14nDY4TFQAa\n" +"LlJ9VM35AApXaLyY1ERrN7u9ALKd2LUwYhM7Km539O4yUFYikE2nIPscEsA5ltpxOgUGCY7b7ez5\n" +"NtD6nL1ZKauw7aNXmVAvmJTcuPxWmoktF3gDJKK2wxZuNGcJE0uFQEG4Z3BrWP7yoNuSK3dii2jm\n" +"lpPHr0O/KnPQtzI3eguhe0TwUem/eYSdyzMyVx/YpwkzwtYL3sR5k0o9rKQLtvLzfAqdBxBurciz\n" +"aaA/L0HIgAmOit1GJA2saMxTVPNhAoGBAPfgv1oeZxgxmotiCcMXFEQEWflzhWYTsXrhUIuz5jFu\n" +"a39GLS99ZEErhLdrwj8rDDViRVJ5skOp9zFvlYAHs0xh92ji1E7V/ysnKBfsMrPkk5KSKPrnjndM\n" +"oPdevWnVkgJ5jxFuNgxkOLMuG9i53B4yMvDTCRiIPMQ++N2iLDaRAoGBAO9v//mU8eVkQaoANf0Z\n" +"oMjW8CN4xwWA2cSEIHkd9AfFkftuv8oyLDCG3ZAf0vrhrrtkrfa7ef+AUb69DNggq4mHQAYBp7L+\n" +"k5DKzJrKuO0r+R0YbY9pZD1+/g9dVt91d6LQNepUE/yY2PP5CNoFmjedpLHMOPFdVgqDzDFxU8hL\n" +"AoGBANDrr7xAJbqBjHVwIzQ4To9pb4BNeqDndk5Qe7fT3+/H1njGaC0/rXE0Qb7q5ySgnsCb3DvA\n" +"cJyRM9SJ7OKlGt0FMSdJD5KG0XPIpAVNwgpXXH5MDJg09KHeh0kXo+QA6viFBi21y340NonnEfdf\n" +"54PX4ZGS/Xac1UK+pLkBB+zRAoGAf0AY3H3qKS2lMEI4bzEFoHeK3G895pDaK3TFBVmD7fV0Zhov\n" +"17fegFPMwOII8MisYm9ZfT2Z0s5Ro3s5rkt+nvLAdfC/PYPKzTLalpGSwomSNYJcB9HNMlmhkGzc\n" +"1JnLYT4iyUyx6pcZBmCd8bD0iwY/FzcgNDaUmbX9+XDvRA0CgYEAkE7pIPlE71qvfJQgoA9em0gI\n" +"LAuE4Pu13aKiJnfft7hIjbK+5kyb3TysZvoyDnb3HOKvInK7vXbKuU4ISgxB2bB3HcYzQMGsz1qJ\n" +"2gG0N5hvJpzwwhbhXqFKA4zaaSrw622wDniAK5MlIE0tIAKKP4yxNGjoD2QYjhBGuhvkWKaXTyY=\n" +"-----END RSA PRIVATE KEY-----\n"; public RTSPResponder(byte[] hwAddr, Socket socket) throws IOException { this.hwAddr = hwAddr; this.socket = socket; in = new BufferedReader(new InputStreamReader(socket.getInputStream())); } public RTSPResponder(byte[] hwAddr, Socket socket, String pass) throws IOException { this.hwAddr = hwAddr; this.socket = socket; this.password = pass; in = new BufferedReader(new InputStreamReader(socket.getInputStream())); } public RTSPResponse handlePacket(RTSPPacket packet) { if(password == null) { // No pass = ok! response = new RTSPResponse("RTSP/1.0 200 OK"); response.append("Audio-Jack-Status", "connected; type=analog"); response.append("CSeq", packet.valueOfHeader("CSeq")); } else { // Default response (deny, deny, deny!) response = new RTSPResponse("RTSP/1.0 401 UNAUTHORIZED"); response.append("WWW-Authenticate", "Digest realm=\"*\" nonce=\"*\""); response.append("Method", "DENIED"); String authRaw = packet.valueOfHeader("Authorization"); // If supplied, check response if(authRaw != null) { Matcher auth = authPattern.matcher(authRaw); if (auth.find()) { String username = auth.group(1); String realm = auth.group(2); String nonce = auth.group(3); String uri = auth.group(4); String resp = auth.group(5); String method = packet.getReq(); String hash1 = md5Hash(username+":"+realm+":"+password).toUpperCase(); String hash2 = md5Hash(method+":"+uri).toUpperCase(); String hash = md5Hash(hash1+":"+nonce+":"+hash2).toUpperCase(); // Check against password if(hash.equals(resp)) { // Success! response = new RTSPResponse("RTSP/1.0 200 OK"); response.append("Audio-Jack-Status", "connected; type=analog"); response.append("CSeq", packet.valueOfHeader("CSeq")); } } } } // Apple Challenge-Response field if needed String challenge; if( (challenge = packet.valueOfHeader("Apple-Challenge")) != null){ // BASE64 DECODE byte[] decoded = Base64.decodeBase64(challenge); // IP byte array //byte[] ip = socket.getLocalAddress().getAddress(); SocketAddress localAddress = socket.getLocalSocketAddress(); //.getRemoteSocketAddress(); byte[] ip = ((InetSocketAddress) localAddress).getAddress().getAddress(); ByteArrayOutputStream out = new ByteArrayOutputStream(); // Challenge try { out.write(decoded); // IP-Address out.write(ip); // HW-Addr out.write(hwAddr); // Pad to 32 Bytes int padLen = 32 - out.size(); for(int i = 0; i < padLen; ++i) { out.write(0x00); } } catch (IOException e) { e.printStackTrace(); } // RSA byte[] crypted = this.encryptRSA(out.toByteArray()); // Encode64 String ret = Base64.encodeBase64String(crypted); // On retire les == ret = ret.replace("=", "").replace("\r", "").replace("\n", ""); // Write response.append("Apple-Response", ret); } // Paquet request String REQ = packet.getReq(); if(REQ.contentEquals("OPTIONS")){ // The response field response.append("Public", "ANNOUNCE, SETUP, RECORD, PAUSE, FLUSH, TEARDOWN, OPTIONS, GET_PARAMETER, SET_PARAMETER"); } else if (REQ.contentEquals("ANNOUNCE")){ // Nothing to do here. Juste get the keys and values Pattern p = Pattern.compile("^a=([^:]+):(.+)", Pattern.MULTILINE); Matcher m = p.matcher(packet.getContent()); while(m.find()){ if(m.group(1).contentEquals("fmtp")){ // Parse FMTP as array String[] temp = m.group(2).split(" "); fmtp = new int[temp.length]; for (int i = 0; i< temp.length; i++){ fmtp[i] = Integer.valueOf(temp[i]); } } else if(m.group(1).contentEquals("rsaaeskey")){ aeskey = this.decryptRSA(Base64.decodeBase64(m.group(2))); } else if(m.group(1).contentEquals("aesiv")){ aesiv = Base64.decodeBase64(m.group(2)); } } } else if (REQ.contentEquals("SETUP")){ int controlPort = 0; int timingPort = 0; String value = packet.valueOfHeader("Transport"); // Control port Pattern p = Pattern.compile(";control_port=(\\d+)"); Matcher m = p.matcher(value); if(m.find()){ controlPort = Integer.valueOf(m.group(1)); } // Timing port p = Pattern.compile(";timing_port=(\\d+)"); m = p.matcher(value); if(m.find()){ timingPort = Integer.valueOf(m.group(1)); } // Launching audioserver serv = new AudioServer(new AudioSession(aesiv, aeskey, fmtp, controlPort, timingPort)); response.append("Transport", packet.valueOfHeader("Transport") + ";server_port=" + serv.getServerPort()); // ??? Why ??? response.append("Session", "DEADBEEF"); } else if (REQ.contentEquals("RECORD")){ // Headers // Range: ntp=0- // RTP-Info: seq={Note 1};rtptime={Note 2} // Note 1: Initial value for the RTP Sequence Number, random 16 bit value // Note 2: Initial value for the RTP Timestamps, random 32 bit value } else if (REQ.contentEquals("FLUSH")){ serv.flush(); } else if (REQ.contentEquals("TEARDOWN")){ response.append("Connection", "close"); } else if (REQ.contentEquals("SET_PARAMETER")){ // Timing port Pattern p = Pattern.compile("volume: (.+)"); Matcher m = p.matcher(packet.getContent()); if(m.find()){ double volume = (double) Math.pow(10.0,0.05*Double.parseDouble(m.group(1))); serv.setVolume(65536.0 * volume); } } else { System.out.println("REQUEST(" + REQ + "): Not Supported Yet!"); System.out.println(packet.getRawPacket()); } // We close the response response.finalize(); return response; } /** * Generates md5 hash of a string. * @param plaintext string * @return hash string */ public String md5Hash(String plaintext) { String hashtext = ""; try { MessageDigest md = MessageDigest.getInstance("MD5"); md.update(plaintext.getBytes()); byte[] digest = md.digest(); BigInteger bigInt = new BigInteger(1,digest); hashtext = bigInt.toString(16); // Now we need to zero pad it if you actually want the full 32 chars. while(hashtext.length() < 32 ) { hashtext = "0"+hashtext; } } catch(java.security.NoSuchAlgorithmException e) { // } return hashtext; } /** * Crypts with private key * @param array data to encrypt * @return encrypted data */ public byte[] encryptRSA(byte[] array){ try{ Security.addProvider(new BouncyCastleProvider()); PEMReader pemReader = new PEMReader(new StringReader(key)); KeyPair pObj = (KeyPair) pemReader.readObject(); // Encrypt Cipher cipher = Cipher.getInstance("RSA/NONE/PKCS1Padding"); cipher.init(Cipher.ENCRYPT_MODE, pObj.getPrivate()); return cipher.doFinal(array); }catch(Exception e){ e.printStackTrace(); } return null; } /** * Decrypt with RSA priv key * @param array * @return */ public byte[] decryptRSA(byte[] array){ try{ Security.addProvider(new BouncyCastleProvider()); // La clef RSA PEMReader pemReader = new PEMReader(new StringReader(key)); KeyPair pObj = (KeyPair) pemReader.readObject(); // Encrypt Cipher cipher = Cipher.getInstance("RSA/NONE/OAEPPadding"); cipher.init(Cipher.DECRYPT_MODE, pObj.getPrivate()); return cipher.doFinal(array); }catch(Exception e){ e.printStackTrace(); } return null; } /** * Thread to listen packets */ public void run() { try { do { System.out.println("listening packets ... "); // feed buffer until packet completed StringBuffer packet = new StringBuffer(); int ret = 0; do { char[] buffer = new char[4096]; ret = in.read(buffer); packet.append(new String(buffer)); } while (ret!=-1 && !completedPacket.matcher(packet.toString()).find()); if (ret!=-1) { // We handle the packet RTSPPacket request = new RTSPPacket(packet.toString()); RTSPResponse response = this.handlePacket(request); System.out.println(request.toString()); System.out.println(response.toString()); // Write the response to the wire try { BufferedWriter oStream = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); oStream.write(response.getRawPacket()); oStream.flush(); } catch (IOException e) { e.printStackTrace(); } if("TEARDOWN".equals(request.getReq())){ socket.close(); socket = null; } } else { socket.close(); socket = null; } } while (socket!=null); } catch (IOException e) { e.printStackTrace(); } finally { try { if (in!=null) in.close(); } catch (IOException e) { e.printStackTrace(); } finally { try { if (socket!=null) socket.close(); } catch (IOException e) { e.printStackTrace(); } } } System.out.println("connection ended."); } }